Ce tutorial reprend le code des codelabs oo-02-snail (escargot instanciable) et oo-01-menu (menu instanciable) et introduit l'encapsulation : protéger les données internes avec private et contrôler l'accès avec des propriétés C#.
À la fin du codelab précédent, la classe Snail a des champs public :
class Snail
{
public int X;
public int Y;
public int Energy;
public string Name;
public ConsoleColor Color;
// ...
}
Le problème : n'importe quel code peut modifier ces champs avec des valeurs absurdes.
À travers 5 étapes progressives, vous apprendrez à :
private pour cacher les champsMenu de la même manièreMontrer concrètement que les champs public permettent la triche.
Dans Program.cs, aprèsRace.Init() et avant la boucle de course, ajoutez ce code temporaire :
// === CODE TRICHEUR (temporaire) ===
Snail tricheur = Race.Snails[0];
// Triche 1 : énergie infinie
tricheur.Energy = 9999;
Console.WriteLine($"{tricheur.Name} a {tricheur.Energy} d'énergie !");
// Triche 2 : victoire instantanée
tricheur.X = Race.FinishX;
Console.WriteLine($"{tricheur.Name} est déjà à la ligne d'arrivée !");
// === FIN DU CODE TRICHEUR ===
Compilez et exécutez. Le premier escargot a 9999 d'énergie et gagne instantanément.
Supprimez le code tricheur avant de continuer. Il a servi à démontrer le problème.
Rendre les champs de Snail inaccessibles depuis l'extérieur.
Remplacez public par private pour tous les champs :
class Snail
{
// Tous les champs deviennent private
private int x;
private int y;
private int energy;
private string name;
private ConsoleColor color;
public Snail(string name, ConsoleColor color)
{
this.name = name;
this.color = color;
this.x = 0;
this.y = 0;
this.energy = 100;
}
// Les méthodes de la classe peuvent toujours accéder aux champs private
public void Draw()
{
SuperConsole.WriteAtColor(this.x, this.y, "@", this.color);
}
public void Erase()
{
SuperConsole.WriteAtColor(this.x, this.y, ".", ConsoleColor.DarkGray);
}
public void Move(int dx, int dy)
{
this.x = this.x + dx;
this.y = this.y + dy;
}
public void ReduceEnergy(int amount)
{
this.energy = this.energy - amount;
if (this.energy < 10)
{
this.energy = 10;
}
}
public void DrawEnergy(int displayX, int displayY)
{
Console.SetCursorPosition(displayX, displayY);
Console.ForegroundColor = this.color;
string bar = new string('#', this.energy / 10);
string empty = new string('-', 10 - this.energy / 10);
Console.Write($"{this.name,6}[{bar}{empty}] ");
Console.ResetColor();
}
}
Essayez de compiler. Vous obtenez de nombreuses erreurs dans Program.cs et Race.cs :
CS0122: 'Snail.energy' is inaccessible due to its protection level
CS0122: 'Snail.x' is inaccessible due to its protection level
CS0122: 'Snail.name' is inaccessible due to its protection level
Partout où le code extérieur accède aux champs de Snail, le compilateur refuse.
| Program.cs (boucle de course) | Lecture du champ privé |
| Program.cs (test victoire) | Lecture du champ privé |
| Race.cs ( | Écriture du champ privé |
| Race.cs ( | Écriture du champ privé |
| Program.cs (affichage gagnant) | Lecture du champ privé |
Rétablir l'accès aux données via des propriétés C# avec le bon niveau de protection.
Avant de coder, réfléchissons à ce dont on a besoin :
| Oui (affichage) | Non (ne change jamais) |
|
| Oui (affichage) | Non (ne change jamais) |
|
| Oui (test victoire) | Non (via |
|
| Oui (possible) | Non (via |
|
| Oui (boucle course) | Non (via |
|
Ajoutez les propriétés au début de la classe, et supprimez les champs correspondants :
class Snail
{
// Propriétés
public string Name { get; }
public ConsoleColor Color { get; }
public int X { get; private set; }
public int Y { get; private set; }
public int Energy { get; private set; }
// Constructeur
public Snail(string name, ConsoleColor color)
{
this.Name = name;
this.Color = color;
this.X = 0;
this.Y = 0;
this.Energy = 100;
}
public void Draw()
{
SuperConsole.WriteAtColor(this.X, this.Y, "@", this.Color);
}
public void Erase()
{
SuperConsole.WriteAtColor(this.X, this.Y, ".", ConsoleColor.DarkGray);
}
public void Move(int dx, int dy)
{
this.X = this.X + dx;
this.Y = this.Y + dy;
}
public void ReduceEnergy(int amount)
{
this.Energy = this.Energy - amount;
if (this.Energy < 10)
{
this.Energy = 10;
}
}
public void DrawEnergy(int displayX, int displayY)
{
Console.SetCursorPosition(displayX, displayY);
Console.ForegroundColor = this.Color;
string bar = new string('#', this.Energy / 10);
string empty = new string('-', 10 - this.Energy / 10);
Console.Write($"{this.Name,6}[{bar}{empty}] ");
Console.ResetColor();
}
}
|
|
|
|
|
|
|
|
Dans Race.Init(), l'initialisation de X et Y utilise private set, ce qui est autorisé uniquement dans la classe Snail. Il faut modifier le constructeur pour accepter la position :
// Constructeur mis à jour
public Snail(string name, ConsoleColor color, int x, int y)
{
this.Name = name;
this.Color = color;
this.X = x;
this.Y = y;
this.Energy = 100;
}
Dans Race.Init() :
public static void Init()
{
Snails = new Snail[]
{
new Snail("Turbo", ConsoleColor.Yellow, StartX, 2),
new Snail("Speedy", ConsoleColor.Cyan, StartX, 4),
new Snail("Flash", ConsoleColor.Magenta, StartX, 6)
};
}
Le code devrait maintenant compiler. La lecture des propriétés fonctionne (snail.Energy, snail.X, snail.Name), mais l'écriture directe est interdite :
snail.Energy = 9999; // ERREUR de compilation !
snail.X = 50; // ERREUR de compilation !
snail.Name = "Hack"; // ERREUR de compilation !
Ajouter une validation dans la propriété Energy pour garantir qu'elle reste toujours dans la plage [10, 100].
Remplacez l'auto-propriété Energy par une full property avec un champ privé :
class Snail
{
private int _energy; // Convention Microsoft : préfixe _
public string Name { get; }
public ConsoleColor Color { get; }
public int X { get; private set; }
public int Y { get; private set; }
public int Energy
{
get { return _energy; }
private set
{
if (value < 10)
_energy = 10;
else if (value > 100)
_energy = 100;
else
_energy = value;
}
}
public Snail(string name, ConsoleColor color, int x, int y)
{
Name = name;
Color = color;
X = x;
Y = y;
Energy = 100; // Passe par la propriété → validé
}
// ...
}
Convention _ : Microsoft recommande le préfixe underscore (_energy) pour les champs privés. Cela évite la confusion avec les propriétés (Energy) et rend this. inutile.
Puisque la validation est dans la propriété, la méthode ReduceEnergy n'a plus besoin de vérifier :
public void ReduceEnergy(int amount)
{
this.Energy = this.Energy - amount; // La propriété gère la limite
}
Si Energy vaut 20 et amount vaut 15, la propriété reçoit value = 5, qui est inférieur à 10, donc energy est mis à 10. La validation est centralisée dans la propriété.
La validation fonctionne partout, automatiquement :
Snail turbo = new Snail("Turbo", ConsoleColor.Yellow, 2, 2);
turbo.ReduceEnergy(95); // 100 - 95 = 5 → clamped à 10
Console.WriteLine(turbo.Energy); // Affiche 10
turbo.ReduceEnergy(50); // 10 - 50 = -40 → clamped à 10
Console.WriteLine(turbo.Energy); // Affiche toujours 10
Appliquer le même traitement d'encapsulation à la classe Menu du codelab oo-01-menu.
class Menu
{
public string[] Options;
public int SelectedIndex;
public Menu(string[] options)
{
this.Options = options;
this.SelectedIndex = 0;
}
public void Display() { /* ... */ }
public void MoveUp() { /* ... */ }
public void MoveDown() { /* ... */ }
}
Menu menu = new Menu(new string[] { "Jouer", "Options", "Quitter" });
menu.SelectedIndex = 999; // Index hors limites !
menu.Options = new string[] {}; // Tableau vide !
menu.SelectedIndex = -5; // Index négatif !
Appliquez les mêmes principes :
class Menu
{
private string[] options;
public int SelectedIndex { get; private set; }
public Menu(string[] options)
{
this.options = options;
this.SelectedIndex = 0;
}
public void MoveUp()
{
if (this.SelectedIndex > 0)
{
this.SelectedIndex = this.SelectedIndex - 1;
}
}
public void MoveDown()
{
if (this.SelectedIndex < this.options.Length - 1)
{
this.SelectedIndex = this.SelectedIndex + 1;
}
}
public void Display()
{
for (int i = 0; i < this.options.Length; i++)
{
if (i == this.SelectedIndex)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($"> {this.options[i]}");
}
else
{
Console.ResetColor();
Console.WriteLine($" {this.options[i]}");
}
}
Console.ResetColor();
}
}
|
| Le tableau ne doit pas être remplacé |
|
| Modifiable uniquement via |
menu.SelectedIndex = 999; // ERREUR de compilation !
menu.options = null; // ERREUR de compilation !
Une propriété peut être calculée à partir d'autres données, sans champ dédié :
// Dans la classe Snail
public bool IsExhausted => this.Energy <= 10;
Utilisation :
if (snail.IsExhausted)
{
Console.WriteLine($"{snail.Name} est épuisé !");
}
=> est une syntaxe raccourcie (expression-bodied member). C'est équivalent à :
public bool IsExhausted
{
get { return this.Energy <= 10; }
}
ToString() avec propriétés// Dans la classe Snail
public override string ToString()
{
return $"{this.Name} (x={this.X}, y={this.Y}, energy={this.Energy})";
}
Console.WriteLine(turbo); // Affiche : Turbo (x=5, y=2, energy=87)
init (C# 9+)L'accesseur init permet d'assigner une valeur uniquement à la création de l'objet, y compris avec la syntaxe d'initialisation :
class Snail
{
public string Name { get; init; }
public ConsoleColor Color { get; init; }
// ...
}
Snail turbo = new Snail("Turbo", ConsoleColor.Yellow, 0, 2)
{
Name = "Super Turbo" // OK : init permet ça à la création
};
turbo.Name = "Hack"; // ERREUR : interdit après la création
Champs |
|
|
Lecture |
|
|
Écriture |
|
|
Modification | Directe, sans contrôle | Via |
Validation | Dans la méthode (contournable) | Dans la propriété (impossible à contourner) |
Nom |
|
|
Convention | — |
|
private cache les données à l'extérieur de la classe{ get; set; }) remplacent les méthodes Get/Set de Java{ get; private set; } = lecture partout, écriture interne seulement{ get; } = immutable après le constructeurvalue représente la valeur assignée dans le setLes données sont protégées, mais toute la logique est dans une seule classe Snail. Que se passe-t-il si on veut des types d'escargots (rapide, lent, à bonus) ? L'héritage permettra de créer des classes spécialisées.